/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.copier.annotations;

import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import ch.simpleel.internal.reflection.*;
import ch.trackedbean.copier.*;
import ch.trackedbean.copier.internal.*;
import ch.trackedbean.loaders.lazy.*;

/**
 * Helper object for building an mapper based on annotations.
 * 
 * @author M. Hautle
 */
public class DefaultBeanMapperConfigurator extends BeanMapperConfigurator {
    /**
     * Default constructor.
     * 
     * @param type The type for which the mapper gets built
     */
    public DefaultBeanMapperConfigurator(Class<?> type) {
        beanMapper = new BeanMapper(getMappedBaseClass(type), type);
    }

    /**
     * Creates and configures the mapper.
     * 
     * @return The created mapper
     * @throws MapperInitialisationException if something went wrong
     */
    public IBeanMapper create() throws MapperInitialisationException {
        try {
            inspectFields();
            inspectPropertyMethods();
            addInheritedMappings();
            cleanUp();
            return beanMapper;
        } catch (MapperInitialisationException e) {
            throw e;
        } catch (Exception e) {
            throw new MapperInitialisationException(e);
        }
    }

    /**
     * Adds mappings inherited from other mappers.<br>
     * At the moment only the mappings of the parent class were added.
     */
    protected void addInheritedMappings() {
        final IBeanMapper mapper = BeanMapperManager.getMapper(beanMapper.getDstType().getSuperclass());
        if (mapper instanceof BeanMapper)
            beanMapper.addMappings((BeanMapper) mapper);
    }

    /**
     * Extracts the value of {@link SourceClass#value()}.
     * 
     * @param type The to type
     * @return The mapped base class or null
     */
    private Class getMappedBaseClass(Class<?> type) {
        final SourceClass src = type.getAnnotation(SourceClass.class);
        return src != null ? src.value() : null;
    }

    /**
     * Inspects the fields of the given class and registers the necessary mappings in the helper.
     * 
     * @throws InstantiationException If something wents wrong
     * @throws IllegalAccessException If something wents wrong
     */
    protected void inspectFields() throws InstantiationException, IllegalAccessException {
        // inspect the fields declared in this class
        for (Field f : beanMapper.getDstType().getDeclaredFields())
            processMappingOf(f.getName(), f, f.getGenericType());
    }

    /**
     * Inspects the properties of the given class and registers the necessary mappings.
     * 
     * @throws IntrospectionException If something went wrong
     * @throws InstantiationException If something went wrong
     * @throws IllegalAccessException If something went wrong
     */
    protected void inspectPropertyMethods() throws IntrospectionException, InstantiationException, IllegalAccessException {
        final Class type = this.beanMapper.getDstType();
        final PropertyDescriptor[] props = Introspector.getBeanInfo(type).getPropertyDescriptors();
        if (props == null)
            return;
        for (PropertyDescriptor d : props) {
            final Method read = d.getReadMethod();
            // skip properties not declared in the current class
            if (read == null || type != read.getDeclaringClass())
                continue;
            final String name = d.getName();
            processMappingOf(name, read, read.getReturnType());
            addDefaultMapping(name, "");
        }
    }

    /**
     * Processes the mapping annotations of the passed element.
     * 
     * @param name The property name
     * @param el The annotated element
     * @param type The value type of annotated element
     */
    protected void processMappingOf(String name, AnnotatedElement el, Type type) {
        if (ignoreMapping(name, el))
            return;
        addSimpleMapping(name, el);
        addExternalMapping(name, el);
        addPathMapping(name, el);
        addDeepMapping(name, el);
        addDeepPathMapping(name, el);
        addCollectionMapping(name, el, type);
        addCollectionPathMapping(name, el, type);
        addMultiMapping(name, el, type);
    }

    /**
     * Adds the mappings for an {@link Mappings} annotation if there is one.
     * 
     * @param name The property name
     * @param el The element
     * @param type The value type of annotated element
     */
    protected void addMultiMapping(String name, AnnotatedElement el, Type type) {
        final Mappings mappings = el.getAnnotation(Mappings.class);
        if (mappings == null)
            return;
        for (DeepMapping m : mappings.deepMappings())
            addDeepMaping(name, m);
        for (DeepPathMapping m : mappings.deepPathMappings())
            addDeepPathMapping(name, m);
        for (CollectionMapping m : mappings.collectionMappings())
            addCollectionMapping(name, m, type);
        for (CollectionPathMapping m : mappings.collectionPathMappings())
            addCollectionPathMapping(name, m, type);
        for (PathMapping m : mappings.pathMappings())
            addPathMapping(name, m);
        for (SimpleMapping m : mappings.simpleMappings())
            addSimpleMapping(name, m);
    }

    /**
     * Checks if the given element should not be mapped. Ensures that a call of {@link #addDefaultMapping(String,String)} does nothig if not desired.
     * 
     * @param name The property name
     * @param el The property element
     * @return True if the element should not inherit it's mapping
     */
    protected boolean ignoreMapping(String name, AnnotatedElement el) {
        final boolean ignore = el.getAnnotation(IgnoreMapping.class) != null;
        if (ignore)
            ignoreMapping(name);
        return ignore;
    }

    /**
     * Adds the mapping for an {@link SimpleMapping} if there is one.
     * 
     * @param property The property name
     * @param el The element
     */
    protected void addSimpleMapping(String property, AnnotatedElement el) {
        final SimpleMapping simple = el.getAnnotation(SimpleMapping.class);
        if (simple != null)
            addSimpleMapping(property, simple);
    }

    /**
     * Adds the mapping for the given {@link SimpleMapping}.
     * 
     * @param property The property name
     * @param simple The annotation
     */
    protected void addSimpleMapping(String property, final SimpleMapping simple) {
        final String src = simple.value();
        final Class<?> srcType = getSourceType("SimpleMapping", property, simple.srcClass());
        final Class<? extends IPropertyConverter> converter = getClass(simple.converter(), IPropertyConverter.class);
        // self mapping?
        if (src.length() == 0)
            addSimpleMapping(srcType, property, converter, simple.profile());
        else
            addSimpleMapping(srcType, src, property, converter, simple.readOnly(), simple.profile());
    }

    /**
     * Adds the mapping for an {@link ExternalMapping} if there is one.
     * 
     * @param property The property name
     * @param el The element
     */
    protected void addExternalMapping(String property, AnnotatedElement el) {
        final ExternalMapping external = el.getAnnotation(ExternalMapping.class);
        if (external == null)
            return;
        for (Class<?> c : external.value())
            addSimpleMapping(c, property, property, external.profile());
    }

    /**
     * Adds the mapping for an {@link DeepMapping} if there is one.
     * 
     * @param property The property name
     * @param el The element
     */
    protected void addDeepMapping(String property, AnnotatedElement el) {
        final DeepMapping deep = el.getAnnotation(DeepMapping.class);
        if (deep != null)
            addDeepMaping(property, deep);
    }

    /**
     * Adds the mapping for an {@link DeepMapping} annotation.
     * 
     * @param property The property name
     * @param deep The annotation
     */
    protected void addDeepMaping(String property, final DeepMapping deep) {
        final String src = deep.srcProperty();
        final Class<?> type = getSourceType("DeepMapping", property, deep.srcClass());
        addDeepMapping(type, src.length() > 0 ? src : property, property, deep.overrideSrc(), deep.overrideDst(),
                getClass(deep.srcEvaluator(), ITypeEvaluator.class), getClass(deep.dstEvaluator(), ITypeEvaluator.class), deep.readOnly(), isLazy(property),
                deep.profile());
    }

    /**
     * Adds the mapping for an {@link DeepPathMapping} if there is one.
     * 
     * @param property The property name
     * @param el The element
     */
    protected void addDeepPathMapping(String property, AnnotatedElement el) {
        final DeepPathMapping deep = el.getAnnotation(DeepPathMapping.class);
        if (deep != null)
            addDeepPathMapping(property, deep);
    }

    /**
     * Adds the mapping for an {@link DeepPathMapping} annotations.
     * 
     * @param property The property name
     * @param deep The annotation
     */
    protected void addDeepPathMapping(String property, final DeepPathMapping deep) {
        final Class<?> type = getSourceType("DeepPathMapping", property, deep.srcClass());
        addDeepPathMapping(type, deep.path(), deep.lastPathType(), property, deep.overrideSrc(), deep.overrideDst(),
                getClass(deep.srcEvaluator(), ITypeEvaluator.class), getClass(deep.dstEvaluator(), ITypeEvaluator.class), deep.readOnly(), isLazy(property),
                deep.profile());
    }

    /**
     * Adds the mapping for an {@link CollectionMapping} if there is one.
     * 
     * @param property The property name
     * @param el The element
     * @param collectionType The type of collection
     */
    protected void addCollectionMapping(String property, AnnotatedElement el, Type collectionType) {
        final CollectionMapping col = el.getAnnotation(CollectionMapping.class);
        if (col != null)
            addCollectionMapping(property, col, collectionType);
    }

    /**
     * Adds the mapping for an {@link CollectionMapping} annotation.
     * 
     * @param property The property name
     * @param col The annotation
     * @param collectionType The type of the collection
     */
    protected void addCollectionMapping(String property, final CollectionMapping col, Type collectionType) {
        final String src = col.srcProperty();
        final Class<?> type = getSourceType("CollectionMapping", property, col.srcClass());
        final Class<? extends ITypeEvaluator> eval = getClass(col.srcEvaluator(), ITypeEvaluator.class);
        if (eval != null)
            addCollectionMapping(type, src.length() > 0 ? src : property, property, eval, isLazy(property), col.profile());
        else
            addStaticCollectionMapping(type, src.length() > 0 ? src : property, property, getCollectionType(property, col.dstType(), collectionType),
                    isLazy(property), col.profile());
    }

    /**
     * Resolves the element type for a {@link Collection}/array.<br>
     * If a destination type is specified (i.e. different from {@link Object}) then this one will be returned. Otherwise the first level of the generic
     * information of the field will be used to figure it out (or simply the element type if it's a normal array) - should this process fail a
     * {@link MapperInitialisationException} will be thrown.
     * 
     * @param property The name of the property holding the collection (only for the error message)
     * @param dstType The destination type specified in the configuration
     * @param collectionType The real field type
     * @return The collection element type
     * @throws MapperInitialisationException If the collection element type could not be resolved
     */
    private Class<?> getCollectionType(String property, Class dstType, Type collectionType) {
        if (dstType != Object.class) {
            return dstType;
        }
        if (collectionType instanceof GenericArrayType) {
            return getActualType(property, ((GenericArrayType) collectionType).getGenericComponentType());
        } else if (collectionType instanceof ParameterizedType) {
            return getActualType(property, collectionType);
        } else if (collectionType instanceof Class && ((Class) collectionType).isArray()) {
            return ((Class) collectionType).getComponentType();
        }
        throw new MapperInitialisationException("The value type of the property " + beanMapper.getDstType().getName() + "." + property
                + " could not be determinated - please specify the dstType!");
    }

    /**
     * Resolves the real type of a {@link ParameterizedType}.<br>
     * This method does only a 'one' level resolution and throws a {@link MapperInitialisationException} if the real type could not be retrieved.
     * 
     * @param property The name of the property (only for the error message)
     * @param type A type
     * @return The real type
     * @throws MapperInitialisationException If the resolution 'failed'
     */
    private Class getActualType(String property, Type type) {
        if (type instanceof ParameterizedType) {
            final Type res = ((ParameterizedType) type).getActualTypeArguments()[0];
            if (res instanceof Class)
                return (Class) res;
        }
        throw new MapperInitialisationException("The value type of the property " + beanMapper.getDstType().getName() + "." + property
                + " could not be determinated - please specify the dstType!");
    }

    /**
     * Adds the mapping for an {@link CollectionPathMapping} if there is one.
     * 
     * @param property The property name
     * @param el The element
     * @param collectionType The type of the collection
     */
    protected void addCollectionPathMapping(String property, AnnotatedElement el, Type collectionType) {
        final CollectionPathMapping col = el.getAnnotation(CollectionPathMapping.class);
        if (col != null)
            addCollectionPathMapping(property, col, collectionType);
    }

    /**
     * Adds the mapping for an {@link CollectionPathMapping} annotation.
     * 
     * @param property The property name
     * @param col The annotation
     * @param collectionType The type of the collection
     */
    protected void addCollectionPathMapping(String property, final CollectionPathMapping col, Type collectionType) {
        final Class<?> type = getSourceType("CollectionPathMapping", property, col.srcClass());
        final Class<? extends ITypeEvaluator> eval = getClass(col.srcEvaluator(), ITypeEvaluator.class);
        if (eval != null)
            addCollectionMapping(type, col.path(), col.lastPathType(), property, eval, isLazy(property), col.profile());
        else
            addStaticCollectionMapping(type, col.path(), col.lastPathType(), property, getCollectionType(property, col.dstType(), collectionType),
                    isLazy(property), col.profile());
    }

    /**
     * Adds the mapping for an {@link PathMapping} if there is one.
     * 
     * @param property The property name
     * @param el The element
     */
    protected void addPathMapping(String property, AnnotatedElement el) {
        PathMapping path = el.getAnnotation(PathMapping.class);
        if (path != null)
            addPathMapping(property, path);
    }

    /**
     * Adds the mapping for an {@link PathMapping} annotation.
     * 
     * @param property The property name
     * @param path The annotation
     */
    protected void addPathMapping(String property, PathMapping path) {
        final Class<?> type = getSourceType("PathMapping", property, path.srcClass());
        addPathMapping(type, property, type, path.path(), getClass(path.converter(), IPropertyConverter.class), path.readOnly(), path.profile());
    }

    /**
     * Checks if the getter of the given property is annotated with {@link LoadLazy}.
     * 
     * @param property The property
     * @return True if the property should be fetched lazy
     */
    protected boolean isLazy(String property) {
        return isLazy(ReflectionHelper.getDescriptor(beanMapper.getDstType(), property));
    }

    /**
     * Checks if the getter of the given property is annotated with {@link LoadLazy}.
     * 
     * @param desc The property descriptor
     * @return True if the property should be fetched lazy
     */
    protected boolean isLazy(PropertyDescriptor desc) {
        final Method read = desc.getReadMethod();
        return read != null && read.getAnnotation(LoadLazy.class) != null;
    }

    /**
     * Returns the source type.<br>
     * Simply substitutes placeholder void.class with {@link IBeanMapper#getMappedBaseType()}.
     * 
     * @param mapping The mapping type (only used for the exception)
     * @param dstProp The destination property (only used for the exception)
     * @param type The type
     * @return The passed type or {@link IBeanMapper#getMappedBaseType()} if it was {@link Object}.class
     * @throws MapperInitialisationException If there is no valid source type
     */
    protected Class<?> getSourceType(String mapping, String dstProp, Class<?> type) {
        final Class res = type != void.class ? type : beanMapper.getMappedBaseType();
        if (res == null)
            throw new MapperInitialisationException(mapping + " on " + beanMapper.getDstType().getName() + "." + dstProp + " is only possible with defined "
                    + SourceClass.class.getName() + " annotation or defined source class!");
        return res;
    }

    /**
     * Substitutes the passed base class through <code>null</code>.
     * 
     * @param <T> The type
     * @param value The class
     * @param undefined The class value representing null
     * @return The passed class or null
     */
    protected <T> Class<? extends T> getClass(Class<? extends T> value, Class<T> undefined) {
        if (value == undefined)
            return null;
        return value;
    }
}